﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information
// 
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 

using System;

using log4net.Core;
using log4net.Appender;
using log4net.Util;
using log4net.Layout;
using System.Text;

namespace log4net.Appender
{
    /// <summary>Logs events to a remote syslog daemon.</summary>
    /// <remarks>
    /// <para>
    /// The BSD syslog protocol is used to remotely log to
    /// a syslog daemon. The syslogd listens for for messages
    /// on UDP port 514.
    /// </para>
    /// <para>
    /// The syslog UDP protocol is not authenticated. Most syslog daemons
    /// do not accept remote log messages because of the security implications.
    /// You may be able to use the LocalSyslogAppender to talk to a local
    /// syslog service.
    /// </para>
    /// <para>
    /// There is an RFC 3164 that claims to document the BSD Syslog Protocol.
    /// This RFC can be seen here: http://www.faqs.org/rfcs/rfc3164.html.
    /// This appender generates what the RFC calls an "Original Device Message",
    /// i.e. does not include the TIMESTAMP or HOSTNAME fields. By observation
    /// this format of message will be accepted by all current syslog daemon
    /// implementations. The daemon will attach the current time and the source
    /// hostname or IP address to any messages received.
    /// </para>
    /// <para>
    /// Syslog messages must have a facility and and a severity. The severity
    /// is derived from the Level of the logging event.
    /// The facility must be chosen from the set of defined syslog 
    /// <see cref="SyslogFacility"/> values. The facilities list is predefined
    /// and cannot be extended.
    /// </para>
    /// <para>
    /// An identifier is specified with each log message. This can be specified
    /// by setting the <see cref="Identity"/> property. The identity (also know 
    /// as the tag) must not contain white space. The default value for the
    /// identity is the application name (from <see cref="LoggingEvent.Domain"/>).
    /// </para>
    /// </remarks>
    /// <author>Rob Lyon</author>
    /// <author>Nicko Cadell</author>
    public class RemoteSyslogAppender : UdpAppender
    {
        /// <summary>Syslog port 514</summary>
        private const int DefaultSyslogPort = 514;

        /// <summary>syslog severities</summary>
        /// <remarks>
        /// <para>
        /// The syslog severities.
        /// </para>
        /// </remarks>
        public enum SyslogSeverity
        {
            /// <summary>system is unusable</summary>
            Emergency = 0,

            /// <summary>action must be taken immediately</summary>
            Alert = 1,

            /// <summary>critical conditions</summary>
            Critical = 2,

            /// <summary>error conditions</summary>
            Error = 3,

            /// <summary>warning conditions</summary>
            Warning = 4,

            /// <summary>normal but significant condition</summary>
            Notice = 5,

            /// <summary>informational</summary>
            Informational = 6,

            /// <summary>debug-level messages</summary>
            Debug = 7
        };

        /// <summary>syslog facilities</summary>
        /// <remarks>
        /// <para>
        /// The syslog facilities
        /// </para>
        /// </remarks>
        public enum SyslogFacility
        {
            /// <summary>kernel messages</summary>
            Kernel = 0,

            /// <summary>random user-level messages</summary>
            User = 1,

            /// <summary>mail system</summary>
            Mail = 2,

            /// <summary>system daemons</summary>
            Daemons = 3,

            /// <summary>security/authorization messages</summary>
            Authorization = 4,

            /// <summary>messages generated internally by syslogd</summary>
            Syslog = 5,

            /// <summary>line printer subsystem</summary>
            Printer = 6,

            /// <summary>network news subsystem</summary>
            News = 7,

            /// <summary>UUCP subsystem</summary>
            Uucp = 8,

            /// <summary>clock (cron/at) daemon</summary>
            Clock = 9,

            /// <summary>security/authorization  messages (private)</summary>
            Authorization2 = 10,

            /// <summary>ftp daemon</summary>
            Ftp = 11,

            /// <summary>NTP subsystem</summary>
            Ntp = 12,

            /// <summary>log audit</summary>
            Audit = 13,

            /// <summary>log alert</summary>
            Alert = 14,

            /// <summary>clock daemon</summary>
            Clock2 = 15,

            /// <summary>reserved for local use</summary>
            Local0 = 16,

            /// <summary>reserved for local use</summary>
            Local1 = 17,

            /// <summary>reserved for local use</summary>
            Local2 = 18,

            /// <summary>reserved for local use</summary>
            Local3 = 19,

            /// <summary>reserved for local use</summary>
            Local4 = 20,

            /// <summary>reserved for local use</summary>
            Local5 = 21,

            /// <summary>reserved for local use</summary>
            Local6 = 22,

            /// <summary>reserved for local use</summary>
            Local7 = 23
        }

        /// <summary>Initializes a new instance of the <see cref="RemoteSyslogAppender" /> class.</summary>
        /// <remarks>
        /// This instance of the <see cref="RemoteSyslogAppender" /> class is set up to write 
        /// to a remote syslog daemon.
        /// </remarks>
        public RemoteSyslogAppender()
        {
            // syslog udp defaults
            this.RemotePort = DefaultSyslogPort;
            this.RemoteAddress = System.Net.IPAddress.Parse("127.0.0.1");
            this.Encoding = Encoding.ASCII;
        }

        /// <summary>Message identity</summary>
        /// <remarks>
        /// <para>
        /// An identifier is specified with each log message. This can be specified
        /// by setting the <see cref="Identity"/> property. The identity (also know 
        /// as the tag) must not contain white space. The default value for the
        /// identity is the application name (from <see cref="LoggingEvent.Domain"/>).
        /// </para>
        /// </remarks>
        public PatternLayout Identity
        {
            get { return this.m_identity; }
            set { this.m_identity = value; }
        }

        /// <summary>Syslog facility</summary>
        /// <remarks>
        /// Set to one of the <see cref="SyslogFacility"/> values. The list of
        /// facilities is predefined and cannot be extended. The default value
        /// is <see cref="SyslogFacility.User"/>.
        /// </remarks>
        public SyslogFacility Facility
        {
            get { return this.m_facility; }
            set { this.m_facility = value; }
        }

        /// <summary>Add a mapping of level to severity</summary>
        /// <param name="mapping">The mapping to add</param>
        /// <remarks>
        /// <para>
        /// Add a <see cref="LevelSeverity"/> mapping to this appender.
        /// </para>
        /// </remarks>
        public void AddMapping(LevelSeverity mapping)
        {
            this.m_levelMapping.Add(mapping);
        }

        /// <summary>This method is called by the <see cref="M:AppenderSkeleton.DoAppend(LoggingEvent)"/> method.</summary>
        /// <param name="loggingEvent">The event to log.</param>
        /// <remarks>
        /// <para>
        /// Writes the event to a remote syslog daemon.
        /// </para>
        /// <para>
        /// The format of the output will depend on the appender's layout.
        /// </para>
        /// </remarks>
        protected override void Append(LoggingEvent loggingEvent)
        {
            try
            {
                // Priority
                int priority = GeneratePriority(this.m_facility, this.GetSeverity(loggingEvent.Level));

                // Identity
                string identity;

                if (this.m_identity != null)
                {
                    identity = this.m_identity.Format(loggingEvent);
                }
                else
                {
                    identity = loggingEvent.Domain;
                }

                // Message. The message goes after the tag/identity
                string message = this.RenderLoggingEvent(loggingEvent);

                Byte[] buffer;
                int i = 0;
                char c;

                StringBuilder builder = new StringBuilder();

                while (i < message.Length)
                {
                    // Clear StringBuilder
                    builder.Length = 0;

                    // Write priority
                    builder.Append('<');
                    builder.Append(priority);
                    builder.Append('>');

                    // Write identity
                    builder.Append(identity);
                    builder.Append(": ");

                    for (; i < message.Length; i++)
                    {
                        c = message[i];

                        // Accept only visible ASCII characters and space. See RFC 3164 section 4.1.3
                        if (((int)c >= 32) && ((int)c <= 126))
                        {
                            builder.Append(c);
                        }
                        // If character is newline, break and send the current line
                        else if ((c == '\r') || (c == '\n'))
                        {
                            // Check the next character to handle \r\n or \n\r
                            if ((message.Length > i + 1) && ((message[i + 1] == '\r') || (message[i + 1] == '\n')))
                            {
                                i++;
                            }
                            i++;
                            break;
                        }
                    }

                    // Grab as a byte array
                    buffer = this.Encoding.GetBytes(builder.ToString());

#if NET_4_5 || NETSTANDARD
                    this.Client.SendAsync(buffer, buffer.Length, this.RemoteEndPoint).Wait();
#else
                    this.Client.Send(buffer, buffer.Length, this.RemoteEndPoint);
#endif
                }
            }
            catch (Exception e)
            {
                this.ErrorHandler.Error(
                    "Unable to send logging event to remote syslog " +
                    this.RemoteAddress.ToString() +
                    " on port " +
                    this.RemotePort + ".",
                    e,
                    ErrorCode.WriteFailure);
            }
        }

        /// <summary>Initialize the options for this appender</summary>
        /// <remarks>
        /// <para>
        /// Initialize the level to syslog severity mappings set on this appender.
        /// </para>
        /// </remarks>
        public override void ActivateOptions()
        {
            base.ActivateOptions();
            this.m_levelMapping.ActivateOptions();
        }

        /// <summary>Translates a log4net level to a syslog severity.</summary>
        /// <param name="level">A log4net level.</param>
        /// <returns>A syslog severity.</returns>
        /// <remarks>
        /// <para>
        /// Translates a log4net level to a syslog severity.
        /// </para>
        /// </remarks>
        protected virtual SyslogSeverity GetSeverity(Level level)
        {
            LevelSeverity levelSeverity = this.m_levelMapping.Lookup(level) as LevelSeverity;
            if (levelSeverity != null)
            {
                return levelSeverity.Severity;
            }

            //
            // Fallback to sensible default values
            //

            if (level >= Level.Alert)
            {
                return SyslogSeverity.Alert;
            }
            else if (level >= Level.Critical)
            {
                return SyslogSeverity.Critical;
            }
            else if (level >= Level.Error)
            {
                return SyslogSeverity.Error;
            }
            else if (level >= Level.Warn)
            {
                return SyslogSeverity.Warning;
            }
            else if (level >= Level.Notice)
            {
                return SyslogSeverity.Notice;
            }
            else if (level >= Level.Info)
            {
                return SyslogSeverity.Informational;
            }
            // Default setting
            return SyslogSeverity.Debug;
        }

        /// <summary>Generate a syslog priority.</summary>
        /// <param name="facility">The syslog facility.</param>
        /// <param name="severity">The syslog severity.</param>
        /// <returns>A syslog priority.</returns>
        /// <remarks>
        /// <para>
        /// Generate a syslog priority.
        /// </para>
        /// </remarks>
        public static int GeneratePriority(SyslogFacility facility, SyslogSeverity severity)
        {
            if (facility < SyslogFacility.Kernel || facility > SyslogFacility.Local7)
            {
                throw new ArgumentException("SyslogFacility out of range", "facility");
            }

            if (severity < SyslogSeverity.Emergency || severity > SyslogSeverity.Debug)
            {
                throw new ArgumentException("SyslogSeverity out of range", "severity");
            }

            unchecked
            {
                return ((int)facility * 8) + (int)severity;
            }
        }

        /// <summary>The facility. The default facility is <see cref="SyslogFacility.User"/>.</summary>
        private SyslogFacility m_facility = SyslogFacility.User;

        /// <summary>The message identity</summary>
        private PatternLayout m_identity;

        /// <summary>Mapping from level object to syslog severity</summary>
        private LevelMapping m_levelMapping = new LevelMapping();

        /// <summary>Initial buffer size</summary>
        private const int c_renderBufferSize = 256;

        /// <summary>Maximum buffer size before it is recycled</summary>
        private const int c_renderBufferMaxCapacity = 1024;

        /// <summary>
        /// A class to act as a mapping between the level that a logging call is made at and
        /// the syslog severity that is should be logged at.
        /// </summary>
        /// <remarks>
        /// <para>
        /// A class to act as a mapping between the level that a logging call is made at and
        /// the syslog severity that is should be logged at.
        /// </para>
        /// </remarks>
        public class LevelSeverity : LevelMappingEntry
        {
            private SyslogSeverity m_severity;

            /// <summary>The mapped syslog severity for the specified level</summary>
            /// <remarks>
            /// <para>
            /// Required property.
            /// The mapped syslog severity for the specified level
            /// </para>
            /// </remarks>
            public SyslogSeverity Severity
            {
                get { return this.m_severity; }
                set { this.m_severity = value; }
            }
        }
    }
}
